#!/usr/bin/env python
#coding=utf-8

'''
Copyright (c) 2014 ksc
author: ksc (http://blog.geekli.cn)
Dependencies:
    python 2.7
    Linux ≥ 2.6.13
    pyinotify 
API http://seb-m.github.io/pyinotify/
'''
import os,sys
import pyinotify
import logging
import hashlib
import time
import ConfigParser
import threading
import thread
import subprocess as subpro

__version__='0.6.1'
__author__='ksc'

if len(sys.argv)>1 and sys.argv[1]=='-v':
    print(__version__)
    sys.exit()

logformat="%(asctime)s %(levelname)s %(message)s"

logging.basicConfig(level=logging.ERROR,format=logformat,filename='km.log',filemode='a')

klog=logging.getLogger('klog')
klog.setLevel(logging.DEBUG)

ch = logging.StreamHandler()
#ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(name)-4s: %(levelname)-4s %(message)s')
ch.setFormatter(formatter)
klog.addHandler(ch)

class EventHandler(pyinotify.ProcessEvent):
    name=None
    backup_path=''
    backup_ext=[]
    backup_maxfilesize=0

    def init(self, name ):
        self.name=name

    def setBackup(self, path, ext, maxfilesize= 1024000):
        ''' set file's ext which need backup'''
        self.backup_path = path
        self.backup_ext  = ext
        self.backup_maxfilesize=maxfilesize

    def process_IN_MODIFY(self, e):
        #print event
        #klog.info("MODIFY :%s"%(event.pathname))
        pass

    def process_IN_CLOSE_WRITE(self, e):
        
        stat=os.stat(e.pathname)
        md5=hashlib.md5(e.pathname+str(time.time()))
        md5=md5.hexdigest()  
        _datepath=time.strftime('%m%d-%y')
        _dst=os.path.join(_datepath, md5)#relative path
        
        info="MODIFY: %s %d %s"%(e.pathname,stat.st_size,_dst)
        klog.info(info)
        if stat.st_size==0 or stat.st_size > self.backup_maxfilesize:
            return
        
        file_ext=os.path.splitext(e.pathname)[1]# may be ''
        if len(self.backup_ext) and file_ext not in self.backup_ext:
            return 
    
        if self.backup_path !='':
            absdatepath=os.path.join(self.backup_path,_datepath)
            if not os.path.isdir(absdatepath):
                os.makedirs(absdatepath)
                klog.debug('MKBACKDIR:%s',absdatepath)
            dst=os.path.join(self.backup_path, _dst)
            
            sh='cp %s %s'%(e.pathname,dst)
            pipe=subpro.Popen(sh, shell=True,stdout=subpro.PIPE,stderr=subpro.PIPE)
            pipe.wait()
            if pipe.returncode:
                _error=pipe.stderr.readline()
                _error='"backupfailed-%s"'%(_dst)
                klog.warn('BACKUP:%s',_error)
                return 
            klog.debug('BACKUP:%s',e.pathname)
        
    def process_IN_DELETE(self, e):
        klog.info("DELETE: %s"%(e.pathname))

    def process_IN_CREATE(self, e):
        #print e
        klog.info("CREATE: %s"%(e.pathname))

    def process_default(self, e):
        #print e
        pass

class myMonitor(object):
    
    def __init__(self, name, path, excl_list=[] ):
        self.name= name 
        self.path= path 
        self.maxfilesize=0
        self.excl_list=excl_list # only dir can be filter
        self.wm= pyinotify.WatchManager()
        
        self.ehandler= EventHandler()
        self.ehandler.init(self.name)

    def setBackup(self, path, ext=[]):
        if isinstance(ext, str):
            ext=ext.split(',')        
            ext=[item.strip() for item in ext if item.strip() !='']
        self.backup_path=path
        self.backup_ext=ext
        klog.debug('SITE[%s] BACKUP_EXT: %s', self.name, ext)
        self.ehandler.setBackup(path,ext)

    def getExcludeList(self):
        if isinstance(self.excl_list, str):
            self.excl_list=self.excl_list.split(';')
        #remove empty item in list
        slist=[item.strip() for item in self.excl_list if item.strip() !='']
        return ['^'+self.path+'/'+item for item in slist] 

    def exit(self):
        self.notifier.stop()    
        klog.info('SITE[%s] exit watch', self.name)

    def start(self):    
        if not os.path.isdir(self.path):
            klog.warn('SITE[%s] path "%s" not exist',self.name, self.path);    
            return False
        #init EventHandler
        self.notifier = pyinotify.ThreadedNotifier(self.wm,self.ehandler)

        klog.debug('SITE[%s] Exclude:%s',self.name, self.getExcludeList())
        excl = pyinotify.ExcludeFilter(self.getExcludeList())
        klog.info('SITE[%s] add watch', self.name)
        self.wm.add_watch(self.path, pyinotify.ALL_EVENTS, rec=True,
                exclude_filter=excl, auto_add=True)
        #self.notifier.loop()
        self.notifier.start()
        klog.info('SITE[%s] start', self.name)
        return True

def getOption(section, option, default=''):
    global cf
    if cf.has_option(section, option):
        return cf.get(section, option)
    else:
        return default
    
script_path=sys.argv[0]
script_dir=os.path.dirname(script_path)

config_file=os.path.realpath(os.path.join(script_dir, 'config.ini'))
if not os.path.isfile(config_file):
    klog.error("config not exit")
    sys.exit(2)

cf = ConfigParser.ConfigParser()
try:
    print config_file
    cf.read(config_file)
except Exception as e:
    logging.error(e)
    sys.exit(1)

logging.info('demon start')
logging.debug('CONFIG:'+config_file)

has_valid=False
mThreads={}
#global config
gbackup_path=getOption('global','backup_path')
gbackup_ext=getOption('global','backup_ext')

for site in cf.sections():
    if site =='global':
        continue
    
    if not cf.has_option(site, 'path'):
        logging.error('SITE[%s] "no path in config file"', site)    
        continue
    path=cf.get(site, 'path')

    excl_list=getOption(site, 'exclude_list')

    backup_path=getOption(site, 'backup_path',gbackup_path)
    backup_ext=getOption(site, 'backup_ext',gbackup_ext)
    
    t = myMonitor(site, path, excl_list )
    t.setBackup(backup_path, backup_ext)
    #continue
    if t.start():
        has_valid=True
        mThreads[site]=t
if not has_valid:
    klog.error("no valid site section in config file")
    sys.exit()
try:
    while 1:
        time.sleep(2)
except KeyboardInterrupt:
    for t in mThreads:
        mThreads[t].exit()

print "main thread exit"
    
